# -*- coding: utf-8 -*-
#
# Copyright 2009 Vanderbilt University
# 
# Licensed under the Apache License, Version 2.0 (the "License"); 
# you may not use this file except in compliance with the License. 
# You may obtain a copy of the License at 
# 
#     http://www.apache.org/licenses/LICENSE-2.0 
# 
# Unless required by applicable law or agreed to in writing, software 
# distributed under the License is distributed on an "AS IS" BASIS, 
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
# See the License for the specific language governing permissions and 
# limitations under the License. 

"""Network API provides primitives for interacting a network.

.. moduleauthor:: John Paulett <john.paulett -at- vanderbilt.edu>
"""

from __future__ import division
from collections import defaultdict
from hornet.network.corenetwork import Node, EdgeDetail
import copy
import networkx
import operator
import random

def transaction_count(graph):
    """Count the number of transactions that occur within the graph. The 
    transactions are found in the 'size' element. If graph is directed and 
    there two edges that are  bidirectional, the sizes of both edges will be 
    added.
    """
    return sum([e[2].size for e in graph.edges_iter(data=True)])

def attribute_count(graph):
    """Returns the number of distinct attributes that form the edges between 
    nodes.
    """
    return graph.meta.attributes

def edge_occurrences(graphs):
    """Counts the occurrences of each edge across a list of graphs."""
    edges = defaultdict(int)
    for graph in graphs:
        for edge in graph.edges_iter(data=True):
            e = edge[0:2]
            edges[e] += 1
    return dict(edges)

def create_directed_graph(**kw):
    """Create a new directed graph object. The optional *kw* will become
    attributes on the graph's meta object."""
    graph = networkx.DiGraph()
    graph.meta = GraphMetaData()
    for k, v in kw.iteritems():
        setattr(graph.meta, k, v)
    return graph

def copy_graph(graph):
    """Returns a deep copy of the graph."""
    # possible speed up if you use cPickle to dump-load
    return copy.deepcopy(graph)

def create_empty_copy(graph):
    """Creates a completely empty copy of *graph*. Useful for making sure
    that you create the same type of graph (directed vs. undirected).
    """
    retval = networkx.create_empty_copy(graph, with_nodes=False)
    retval.meta = copy.deepcopy(graph.meta)
    return retval

def create_edge(node1, node2, **kw):
    """Creates a new edge in the form *node1* -> *node2*. The function is the 
    same for both directed and undirected edges.
    
    Additional arguments are set as attributes to the edge.
    """
    details = EdgeDetail()
    ## put the keyword arguments into the EdgeDetail 
    for k, v in kw.iteritems():
        setattr(details, k, v)
    return (node1, node2, details)
    
def add_edge(graph, node1, node2, **kw):
    """Creates a new edge of the form *node1* -> *node2*, adding a reference to 
    it into *graph* and always *nodeA*. If the graph is undirected, *nodeB* 
    also gets a reference to the edge. The values for this edge's EdgeDetails
    can be specified as *kw*.
    
    Returns the newly created edge.
    """
    e = create_edge(node1, node2, **kw)
    graph.add_edge(*e)
    return e
    
def remove_edge(graph, edge):
    """Safely removes all references to the edge in the graph."""
    node1, node2 = edge[0:2]
    graph.delete_edge(node1, node2)

def sort_similarity(tosort):
    """Sort a list of edges according to the similarity of the two nodes.
    Assumes the edge has a similarity properties set.
    """
    return sort_edge_details(tosort, 'similarity')

def sort_edge_details(tosort, attribute):
    """Sort a list of edges according to the *attribute* of EdgeDetail in
    each edge.
    """
    attr = lambda e: getattr(e[2], attribute)
    compare = lambda x, y: cmp(attr(x), attr(y))
    return sorted(tosort, cmp=compare, reverse=True)
    
def sort_random(tosort):
    """Performs a random shuffle of the *tosort* list. This function modifies
    *tosort*.
    """
    random.shuffle(tosort)
    return tosort

def sort_no_op(tosort):
    """Sort function that simply returns the exact list to sort."""
    return tosort

def rule_generation(graph, denominator_fn=attribute_count):
    """Generates association rules for the graph. Assumes a directed graph, and 
    is 'destructive' in that it assigns a confidence and a support to each edge
    in the graph.
    
        * *denominator_fn* is the function on which to normalize the edge 
          sizes by. By default, :func:`hornet.network.attribute_count` is used,
          but :func:`hornet.network.transaction_count` is also valid.
    """
    denominator = denominator_fn(graph)
    for edge in graph.edges_iter(data=True):
        assign_rule_probabilities(edge, denominator)
        
def assign_rule_probabilities(edge, denominator):
    """Compute the joint and conditional probabilities for an edge based upon
    the size of the first node, the size of the intersection, and the 
    *denominator* (a function of the entire network). This function modifies
    the *edge* object.
    
    Note that none of the sizes nor the *denominator* should be zero (or else
    the are not really an edge or a node).
    """
    edge[2].joint = edge[2].size / denominator
    edge[2].conditional = edge[2].size / edge[0].size

def directionality(graph):
    """Computes the directionality of all edges in *graph*.
    Return a dictionary keyed by edges with the values as the directionality
    of that edge. 
         
    >>> graph = create_directed_graph()
    >>> a, b = Node('a') , Node('b')
    >>> e1 = add_edge(graph, a, b, joint=0.5)
    >>> e2 = add_edge(graph, b, a, joint=0.25)
    
    >>> result = directionality(graph)
    >>> result[e1[0:2]]
    2.0
    >>> result[e2[0:2]]
    0.5
    """
    directions = {}
    for edge in graph.edges_iter(data=True):
        e = edge[0:2]
        if graph.has_edge(e[1], e[0]):
            # both directions exist, so we can do the calculation
            ratio = edge[2].joint / graph[e[1]][e[0]].joint
        else:
            # the return edge does not exist, so the edge only occurs in this
            # direction
            ratio = float('inf')
        directions[e] = ratio
    return directions
    
class GraphMetaData(object):
    """Empty structure for storing graph details within graph.meta"""
